#om1-assembly

class Symbol_table_extra 
    attr_accessor :offset, :update 

    def initialize(offset = -1, update = true) 
        @offset = offset 
        @update = update 
    end 
end 

class Assembler 
    attr_reader :machine_code, :error_list, :offset 

    @@GLOBAL_ARGS = { 
        :parse_add  => 1, :parse_ld  => 1, :parse_ldi => 1, :parse_st   => 1, :parse_jmp  => 1, 
        :parse_bre  => 2, :parse_brn => 2, :parse_brp => 2, :parse_br_  => 2, :parse_halt => 0, 
        :parse_push => 0, :parse_pop => 0, :parse_jsr => 1, :parse_occu => 1, :parse_fill => 1 
    }

    def initialize
        #assembler information 
        @line = 1  
        @offset = 0  
        @symbol_table = {} 
        @error_list = [] 

        #run-time information 
        @state = :parse_start

        #destination file 
        @machine_code = []
    end 

    #assembling ... 
    def assemble(code)

        code.each_line do |instr| 
            args = 0 
            pre_cmt_state = nil
            @state = :parse_start 

            instr.split(' ').each do |word| 
                #puts "#{@line} / #{@offset} : #{word} - #{@state}"
        
                case word 
                when 'add' 
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x00 
                        @state = :parse_add 
                    end 
                when 'ld' 
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x01 
                        @state = :parse_ld 
                    end 
                when 'ldi'
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x02 
                        @state = :parse_ldi 
                    end 
                    
                when 'st'
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x03 
                        @state = :parse_st 
                    end 

                when 'jmp'
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x04 
                        @state = :parse_jmp 
                    end 

                when 'bre'
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x05 
                        @state = :parse_bre 
                    end 

                when 'brn'
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x06 
                        @state = :parse_brn 
                    end 

                when 'brp'
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x07 
                        @state = :parse_brp 
                    end 

                when 'halt'
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x08 
                        @state = :parse_halt 
                    end 

                when 'push' 
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x09 
                        @state = :parse_push 
                    end 

                when 'pop' 
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x0a 
                        @state = :parse_pop  
                    end 

                when 'jsr'
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @machine_code << 0x0b 
                        @state = :parse_jsr 
                    end 

                when '>' 
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @state = :parse_fill 
                    end 

                when '@' 
                    if @state != :parse_start 
                        @error_list << "#{@line} : more operations in one instruction"
                        @machine_code << :ei  
                        @state = :parse_exception 
                    else 
                        @state = :parse_occu  
                    end 

                when /;.*/ 
                    break 

                when /:.*/
                    @symbol_table[word[1..-1]] = Symbol_table_extra.new(-1, true) unless @symbol_table[word[1..-1]]
        
                    pre = @symbol_table[word[1..-1]].offset 
                    while pre != -1 
                        tmp = @machine_code[pre] 
                        @machine_code[pre] = @offset 
                        pre = tmp 
                    end 
        
                    @symbol_table[word[1..-1]].offset = @offset 
                    @symbol_table[word[1..-1]].update = false 
        
                    @offset -= 1 # pseudo instruction doesn't occupy memory space 
                else  
					#enumeric = word.to_i 
				
                    case @state 
                    when :parse_comment 
                        # deal_with comment ...  
                    when :parse_fill 
                        if isEnglish(word) 
                            @error_list << "#{@line} : fill > arg must be numeric" 
                            @machine_code << :ef  
                            @state = :parse_exception 
                        else 
                            @machine_code << (word.to_i > 0 ? word.to_i : word.to_i & 0xff) 
                            @offset -= 1
                        end
                    when :parse_occu  
                        if isEnglish(word) 
                            @error_list << "#{@line} : occu @ arg must be numeric" 
                            @machine_code << :eoa 
                            @state = :parse_exception 
                        else 
                            occu_len = word.to_i 
                            #raise "dup must more than 0" if word.to_i <= 0
                            if occu_len <= 0 
                                @error_list << "#{@line} : occu @ must more than 1 space" 
                                @machine_code << :eon 
                            else 
                                occu_len.times { @machine_code << 0 } 
                                @offset += occu_len - 1
                            end 
                        end 
                    when :parse_ldi, :parse_add 
                        if isEnglish(word) 
                            @error_list << "#{@line} : can't use symbol in instr_add or instr_ldi" 
                            @machine_code << :ef 
                            @state = :parse_exception 
                        else 
                            @machine_code << (word.to_i > 0 ? word.to_i : word.to_i & 0xff) 
                        end 
                    when :parse_ld, :parse_st, :parse_jmp, :parse_br_, :parse_jsr 
                        if isEnglish(word) 
                            @symbol_table[word] = Symbol_table_extra.new(-1, true) unless @symbol_table[word] 
                
                            @machine_code << @symbol_table[word].offset 
                            @symbol_table[word].offset = @offset if @symbol_table[word].update 
                        else 
                            if word.to_i < 0
                                @error_list << "#{@line} : oprand can't be negtive" 
                                @machine_code << :enn 
                            else 
                                @machine_code << word.to_i 
                            end 
                        end 
                    when :parse_bre, :parse_brn, :parse_brp    
                        @machine_code << (word.to_i > 0 ? word.to_i : word.to_i & 0xff) 
                    end 
        
                    @state = :parse_br_ if (@state == :parse_bre) || (@state == :parse_brn) || (@state == :parse_brp)    
                    args += 1
        
                end 
        
                @offset += 1 
        
            end 
            
            unless @state == :parse_start || @state == :parse_exception 
                @error_list << "#{@line} : much_args" if args > @@GLOBAL_ARGS[@state] 
                @error_list << "#{@line} : less_args" if args < @@GLOBAL_ARGS[@state] 
            end 
            
            @line += 1 
        end  

        @symbol_table.each { |k, v| @error_list << "can't find label <#{k}>" if v.update }

        return @error_list.empty? 
    end 

    #check the token whether a numeric or a symbol 
    def isEnglish(word) 
        state = :start 
        word.each_char do |ch| 
            case state  
            when :start 
                case ch 
                when '-', '0'..'9'
                    state = :digit 
                else 
                    state = :else  
                end 
            when :digit 
                state = ('0' <= ch && ch <= '9') ? :digit : :else  
            end 
        end 
    
        return (state == :digit) ? false : true 
    end
end 

class Receiver 
    attr_reader :src_file, :des_file, :code 

    def initialize 
        @src_file = ""
        @des_file = "" 
        @code     = ""
    end 

    def get_name_of_files 
        state = :rcv_start 

        ARGV.each do |arg|     

            case arg 
            when '-o' 
                state = :rcv_o 
            when '-s' 
                state = :rcv_s 
            else 
                case state 
                when :rcv_start 
                    puts "invalid arguments #{arg}" 
                    return false 
                when :rcv_o 
                    @des_file = arg 
                when :rcv_s 
                    if File.file? arg 
                        @src_file = arg 
                    else 
                        puts "no such file #{arg}" 
                        return false 
                    end 
                end 
            end 
        end 

        return !(@src_file.empty? || @des_file.empty?)
    end

    def get_content 
        File.open(@src_file, "r") do |file| 
            file.each { |line| @code << line } 
        end 
    end 

    def output_file(machine_code) 
        machine_code.unshift(machine_code.length)

        # i know where the problem there 
        # in c program, file-io has two way to write "\n" to file 
        # one way is only write "\n"
        # and another is no only write "\n", and also "\r" 
        # yes,  the "\r" is just 0x0D

        # it's soooooo strange ... 
        # in text mode, you write "\n" as "\r\n" 
        # in bin mode, you write "\n" just as "\n" 
        IO.binwrite(@des_file, machine_code.pack('C*'))
    end 
end 



rcv = Receiver.new ; assembler = Assembler.new 

puts "+ geting source code ... "
return unless rcv.get_name_of_files ; rcv.get_content 

puts "+ assembling ... " 
if assembler.assemble(rcv.code)  
    rcv.output_file(assembler.machine_code) 
    puts "+ done (#{assembler.machine_code.length} bytes) ... " 

    print "\n+ machine_code : \n", assembler.machine_code
else
    puts "\n+ assembler failure : error (#{assembler.error_list.length})" 
    assembler.error_list.each { |e| puts '+--> ' + e } 
end 






